<?php
/**
 * Parser
 *
 *     SCMwiki Copyright (C) 2010  Callan Bryant <callan1990@googlemail.com>
 *
 *     Based on Voswork - A simple, fast PHP filesystem abstraction layer
 *     Voswork Copyright (C) 2010  Callan Bryant <callan1990@googlemail.com>
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * @package main
 * @author Callan Bryant <callan1990@googlemail.com>
 */

/**
 * parser class
 * 
 * Simply parses a wiki page into an article object with metadata.
 * 
 * the parser also generates metadata for the article, in the attributes.
 *
 * user scmwikiCrawler class to load articles via page name, cached.
 * 
 * THE OBJECT RETURNED REPRESENTS THE ARTICLE IN STRING FORM
 *
 * @package core
 * @author Callan Bryant
 */
class parser
{
	// how to link to a wiki page
	const PAGE_LINK_PREFIX = '?';
	// summary of page, as/if depicted by pragma
	public $summary = null;
	// page that has to be redirected to (if redirect exists)
	public $redirect = null;
	// output from the parser
	protected $article;
	// name of article (filename)
	public $name;
	// etag of file loaded, to make sure latest version is loaded on wakeup
	public $etag;
	// original article source filesize in bytes (approx, as cleaned)
	public $size;
	// last edit unix timestamp
	public $modified;
	// loaded from cache?
	public $deserialized = false;
	// article source DURING A PARSE ONLY
	protected $source;
	protected $filepath;
	// array of tags for search etc
	public $tags = array();
	// list of wiki pages linked to in this document
	//public $outgoing = array();
	// markdown parser
	protected static $markdown; 

	/**
 	 * @param string $filepath file to parse
	 */
	public function __construct($filepath)
	{
		// set article name
		$file = basename($filepath);
		$this->name = self::match_once('/(.+)\.[[:alnum:]]+/ui',$file);

		$this->filepath = &$filepath;

		// load an up to date copy of the source
		$this->article = @file_get_contents($filepath);

		if ($this->article === false)
			throw new Exception("Failed to load $filepath - check permissions");

		// ensure an instance of the markdown parser
		if (!isset($this->markdown) )
			$this->markdown = new Markdown_Parser();

		// DESTROY HTML SCRIPT TAGS
		if (!ALLOW_JS)
			$this->article = preg_replace('/<script.*>.*<\/script>/Uis',null,$this->article);

		// CONVERT LINE  ENDINGS TO  UNIX FORMAT (LF)
		$bad_endings = array("\r\n","\r");
		$this->article = str_replace($bad_endings,"\n",$this->article);

		// get the metadata, used here and externally		
		$this->take_metadata();

		$this->parse_tables();

		// convert the text with the markdown parser
		$this->article = $this->markdown->transform($this->article);

		// look for remaining wiki links eg: [page]
		$this->replace('/\[(.+)\]/U','<a href="'.self::PAGE_LINK_PREFIX.'$1">$1</a>');

		// remove any excessive whitespace
		$this->article = preg_replace('/(\s)\s+/','$1',$this->article);
	}

	/**
	 * parses tables - delimited by whitespace (tabs or 2+ spaces)
	 */
	protected function parse_tables()
	{
		// specific function to replace whitespace table
		$callback = function($block)
		{		

			// split into individual lines (rows)
			// !!! -1 is to ignore the last blank newline !!!
			$rows = explode("\n",$block,-1);

			// convert each row into an array of cells
			foreach ($rows as &$row)
				$row = preg_split('/[ \t]{2,}|\t/',$row);

			// make sure pointer is at beginning of array
			reset($rows);

			$html = '<table>';

			// the first row as header cells...
			$html	.= '<tr>';
			
			foreach (current($rows) as $cell)
			$html	.= '<th>'.$cell.'</th>';

			$html	.= '</tr>';	


			// remaining cells as rows
			while (next($rows))
			{
				$html	.= '<tr>';
			
				foreach (current($rows) as $cell)
					$html	.= '<td>'.$cell.'</td>';

				$html	.= '</tr>';		
			}

			$html		.= '</table>';

			return $html;
		};

		$this->replace_callback('/(((.+(\t))+.+\n)+)/',$callback);

	}

	/**
	 * sets other attributes based on information from the source
	 */
	protected function take_metadata()
	{
		// summary
	 	$this->summary = self::match_once('/^@summary (.+)$/mU',$this->article);

		// redirect (just reads URL into attribute, act upon externally)
		$this->redirect = self::match_once('/^@redirect (.+)$/mU',$this->article);

		// file based metadata
		$this->etag = httpResponse::etag($this->filepath);
		$this->size = strlen($this->article);
		$this->modified = filemtime($this->filepath);

		// tags
	 	$this->tags = explode(',',self::match_once('/^@tags (.+)$/mU',$this->article));
		// the page name should also be a tag
		$this->tags[] = $this->name;

		// remove any pragma
		$this->replace('/^@.*$/mU', null);
	}

	/**
	 * Wrapper: searches the working copy (article) for a regex and replaces
	 * with the given string (which may contain backreferences)
	 * 
	 * @param string $regex to search for
	 * @param string $replacement string to replace with
	 */
	public function replace($regex,$replacement)
	{
		$this->article = preg_replace($regex,$replacement,$this->article);
	}

	/**
	 * Wrapper: searches the working copy (article) for a regex and replaces
	 * the full match with a callback, given the FIRST GROUP!
	 * 
	 * The given callback must accept a matched (first group!) string as
	 * its only parameter, and return the replacement.
	 * 
	 * It is recommended that the callback is to be a anonymous function;
	 * as it will almost certainly be to bespoke to be re-usable.
	 * 
	 * @param string $regex to search for
	 * @param callback $callback
	 */
	public function replace_callback($regex,$callback)
	{
		// get an array of FULL PATTERN matches
		$matches = array();
		preg_match_all($regex,$this->article,$matches,PREG_PATTERN_ORDER);
		$matches = $matches[0];
		
		foreach ($matches as &$match)
		{
			// find first group of match
			$first = self::match_once($regex,$match);

			// replace matches with the output of its respective callback,
			// giving it the first group as the parameter
			$this->article = str_replace($match,call_user_func($callback,$first),$this->article);
		}
	}

	/**
	 * simple wrapper for preg_match
	 * returns the first thing that matches the first backreference
	 * @param string $regex regex, with one backreference, to matches
	 * @param string &$haystack
	 * @return string first match
	 */
	public static function match_once($regex,&$haystack)
	{
		$matches = array();
		preg_match($regex,$haystack,$matches);
		
		// return the first backreference (index 0 is the entire expression)
		if (isset($matches[1]))		
			return $matches[1];
		else
			return false;
	}
	  
	/**
	 * simple wrapper for preg_match_all
	 * returns an array of first backreference matches
	 * @param string $regex regex, with one backreference, to matches
	 * @param string &$haystack
	 * @return array first backreference matches
	 */
	public static function match_all($regex,&$haystack)
	{
		$matches = array();
		preg_match_all($regex,$haystack,$matches,PREG_PATTERN_ORDER);

		//return array of first set of matches
		if (isset($matches[1]))
			return $matches[1];
		else
			return false;
	}

	public function __wakeup()
	{
		$this->deserialized = true;
	}

	// is this article up-to-date? (EG: if loaded from cache, it could have
	// been modified since)
	public function is_current()
	{
		return ($this->etag == httpResponse::etag($this->filepath));
	}

	/**
	 * MAGIC: returns the actual article. Metadata is accessed by attributes
	 *
	 */
	public function __toString()
	{
		return $this->article;
	}
}
?>
